#!/usr/bin/python2.4
#
# -*- coding: utf-8 -*-
#
###############################################################
# Copyright 2006 Google Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
###############################################################


"""A JSON feed objectifier.

It maps JSON feed raw data to local pythonic object structure
(I call it OBJson object) and provide a number of methods to access/
modify each object. Dynamically create python objects as necessary
using a superclass and a factory method.

  OBJson: superclass
    Model class called everytime when a cjson decoded JSON feed gets
    sent to the factory method. It also can be instantiated directly
    to create the top level object.
    All subclasses inherited from this superclass.

    The rule of converting:
      1. Only dict structure in JSON feed will be converted to a python object.
      The key of the dict will be the object attributes and the value
      will then be linked to each attribute respectively. The process
      is recursive until it hits String type values.
      2. If a key has '$' in it, for example, 'ns$timezone', the corresponding
      class name will be using '___' to replace the '$' sign since '$' is
      illegal in class name.
    
    A little bit of namespace handling:
      Methods provide here do not do a whole lot of error or integrity checking.
      Since its a pretty generic JSON data handling tool, the namespace handling
      has very low restrictions. As mapping to the rules of XML representation, a
      namespace declaration can be defined at any level of the element. Its the
      best practice for the programmers to maintain the integrity of the whole
      structure. For example, before adding a new child to an existing object
      structure, you should add namespace declarations first if the new child 
      introduces new namespace aliases. For example:
      
      ns = objutils.NameSpace(obj)
      if new_ns_key in ns.keys():
        obj.CreateChild(child_node)
      else:
        # you can even avoid this if you know where to add
        ns_node = objutils.GetNSNode(obj)
        if len(ns_node) == 1:   # because it allows namespace declaration on
          the_node = ns_node[0] # multiple level of JSON nodes.
          the_node.AddNameSpace(new_ns)
        obj.CreateChild(child_node)


  object methods:
  __init__:     constructor. Initialized the object.
  _ParseFile:   If input is file object, read content.
  _parse:       main function to parse the data and convert to OBJson object.
  __iter__:     make the object iterable.
  __len__:      object has length according to the size of attribute.
  get:          get method.
  set:          set method.
  text:         property to get/set/del a '$t' value.
  ParentObj:    property manipulating parent object.
  GetAttrs:     a generator on all attributes.
  GetName:      get object class name.
  AddNameSpace: Add namespace declaration(s) to an OBJson object.
  DelNameSpace: Delete namespace declaration(s) to an OBJson object.
  UpdateNameSpace: update an existing namespace declaration.
  HasChild:     boolean to see if a object has sub OBJson objects.
  GetAttrib:    Return object attributes which do not point to an OBJson
                object.
  GetChildren:  returns all sub OBJson objects.
  AppendChild:  append a child OBJson object to the current object.
  InsertChild:  insert a child OBJson object to the current object.
  UpdateChild:  update/replace an existing OBJson object to the given one.
  DeleteChild:  delete a child object from the current object.
  MakeClone:        make a clone of the current object using object deepcopy.

  Public Functions:
  genpyobj:     subclass and object factory. It creates subclass from the given
                type and also generate an object of it. It posts the class and
                object to the global namespaces so rest of the code can see
                them.
"""


__author__ = 'i3dmaster@gmail.com'


import __builtin__
import os.path
import sys
import types
import copy
import warnings
from cStringIO import StringIO
import cjson


_VMINOR = sys.version_info[1]

if _VMINOR <= 4:
  # running python 2.4 version
  import struct

# define some Exceptions
class Error(Exception):
  pass


class  OBJsonError(Error):
  pass


class OBJsonValueError(Error):
  pass


class OBJsonAttributeError(Error):
  pass


class OBJsonUnknownError(Error):
  pass


class OBJsonRuntimeError(Error):
  pass


class OBJsonNoKeyFoundError(Error):
  pass


class OBJsonTypeError(Error):
  pass


# construct a meta class that will be used to create classes dynamically
class OBJson(object):
  """OBJson super class for creating subclasses.

  All JSON children (dict objects) will be constructed to a OBJson subclass
  by using it as a base class. subclasses will try to use the key as the class
  name (except $ => ___) and the child will be wrapped into the class object.
  """
  def __init__(self, src=None, parent=None):
    """Object constructor. Initialize the object and automatically
       call the internal parse function.

    Args:
      src: A passed in data source. It can be in following formats:
           1. file-list object. A python file object.
           2. JSON data string. For example, a JSON feed response body.
              This will get converted to a StringIO object which is actually
              a file-like object. It supports read() function.
           3. Python data structure such as cjson parsed JSON feed. This will
              be handed over directly to _parse function.
              The first three will be read in using file read() function and
              parsed by cjson to a python dict object, and then will hand over
              to _parse to objectify.
           4. a file containing JSON feed strings.

    Raises:
      ObjsonRuntimeError, when none of the above data format can be
      applied.
    """
    self.__parent = parent

    if isinstance(src, OBJson):
      raise OBJsonTypeError, "%s is already an objson object" % src
    elif src is None:
      # when pass in source is None
      # treat it as an empty dict and meaning to create
      # a fresh new OBJson object.
      src = {}
    elif type(src) in (types.ListType, types.DictType):
      # pass in src is a cjson converted dict object
      # src will directly passed to the internal parser.
      self._parse(src)
    elif hasattr(src, 'read'):
      # If src is a file-like object.
      self._ParseFile(src)
    elif os.path.isfile(os.path.abspath(src)):
      self._ParseFile(open(src,'rb'))
    elif type(src) in (types.StringType, types.UnicodeType):
      # If src is a string like object
      self._ParseFile(StringIO(src))
    else:
      # Source is something can not recognized.
      raise OBJsonValueError, 'Error on parsing the data'

  def __repr__(self):
    if _VMINOR <= 4:
      h = '%#x' % (long(__builtin__.id(self)) + \
          (1L << struct.calcsize('P') * 8))
    else:
      h = '%#x' % __builtin__.id(self)
    if self.__class__.__name__.find('___') != -1:
      return '<%s:%s>' % (self.__class__.__name__.replace('___','$'), h)
    else:
      return '<%s:%s>' % (self.__class__.__name__, h)

  def __str__(self):
    if '___t' in self.GetAttrs:
      return '%s' % getattr(self,'___t')
    else:
      return repr(self)

  def _ParseFile(self, src):
    """Parse content of the file object using cjson module.

    This is will convert the JSON raw data feed to python dict strucutre.

    Args:
      src: a file object.

    Raises:
      OBJsonRuntimeError, when it can't parse the data.
    """
    try:
      data = cjson.decode(src.read())
    except:
      # maybe you passed a XML (evil) feed?
      raise OBJsonRuntimeError, 'Can not parse JSON description'
    src.close()
    self._parse(data)

  def _HandleList(self, key, alist):
    """Handle a list structure of a JSON element.

    Args:
      key: the original dict key pointing to the list.
      alist: the list.

    Return:
      a new list where structures underneath gets transferred to
      OBJson objects.
    """
    new_list = []
    for i in alist:
      if isinstance(i, (basestring, int, float, long)):
        new_list.append(i)
      elif type(i) is types.DictType:
        pyobj = genpyobj(key, i, self)
        new_list.append(pyobj)
      else:
        new_list.append(self._HandleList(key, i))
    return new_list

  def _parse(self, data):
    """Method to convert python dict structure to OBJson object.

    Args:
      data: python dict.
    """
    # The data MUST be a subclass of dict
    if not data:
      # pass in data is an empty dict
      # meaning it is asking to create a new child object
      # nothing to parse. add/edit methods will handle add
      # edit objects underneath.
      pass
    else:
      if type(data) is types.DictType:
        for k, v in data.iteritems():
          if isinstance(v, (basestring, int, float, long)):
            if k == '$t': k = '___t'
            setattr(self, k, v)
          elif type(v) is types.DictType:
            pyobj = genpyobj(k, v, self)
            setattr(self, k, pyobj)
          else:
            setattr(self, k, self._HandleList(k, v))

  def __iter__(self):
    """An iterator method.

    Returns:
      Iterator object to return key,value pair
    """
    return ((k, v) for k, v in self.__dict__.iteritems())

  def __len__(self):
    """Length method to return the number of attributes.

    Returns:
      The number of attributes for this object.
    """
    return len(self.__dict__)

  def get(self, attr):
    """retrieve value of an object attribute.

    Args:
      attr: attribute name used to retrieve its value.

    Raises:
      OBJsonAttributeError, when there is no much attribute found.
    """
    try:
      return getattr(self, attr)
    except:
      raise OBJsonAttributeError, '%s does not have attribute %s' % \
          (self, attr)

  def set(self, attr, value):
    """set an attribute value to new value.

    Args:
      attr: attribute needs to be set
      value: the new value
    """
    setattr(self, attr, value)

  def _GetText(self):
    """Internal method to get the text value of an object.
    """
    if '___t' in self.GetAttrs:
      return self.get('___t')
    else:
      return None

  def _SetText(self, value):
    """Set a text attribute of an OBJson object to value.

    Args:
      value: the text value.
    """
    self.set('___t', value)

  def _DelText(self):
    """Remove a text attribute out of an OBJson object.
    """
    del self.__dict__['___t']

  text = property(_GetText, _SetText, _DelText, doc='Property to manipulate' \
      ' text attribute of a given object')

  @property
  def GetAttrs(self):
    """Property function to return attribute list.

    Read-only attribute.

    Returns:
      a generator to return attribute names.
    """
    return (k for k in self.__dict__.keys())

  def _DoGetParent(self):
    """Function to define the parent read-only property

    Returns:
      The parent object of current object.
    """
    try:
      return self.__parent
    except:
      return None

  def _DoSetParent(self, obj):
    """Set the current object's parent object.

    Args:
      obj: an OBJson object that will be assigned as the parent object.
    """
    if obj is None:
      setattr(self, '_OBJson__parent', None)
    elif isinstance(obj, OBJson):
      if obj.ParentObj is not None and obj.ParentObj == self:
        raise OBJsonRuntimeError, 'Parent refernece looping, aborting'
      else:
        setattr(self, '_OBJson__parent', obj)
    else:
      raise OBJsonTypeError, 'The pass-in object %s is not an OBJson object' \
            % obj

  def _DoDelParent(self):
    """Remove the parent reference of the current object.
    
    Simply set it to None.
    """
    if hasattr(self, '_OBJson__parent'):
      setattr(self, '_OBJson__parent', None)

  ParentObj = property(_DoGetParent, _DoSetParent, _DoDelParent,
                       doc='OBJson parent object property')

  @property
  def GetName(self):
    """Function to return the object node name

    Returns:
      OBJson object node name
    """
    # Note that this does not return the actual class
    # name if the object node name has $ in it.
    if self.__class__.__name__.find('___') != -1:
      return self.__class__.__name__.replace('___','$')
    else:
      return self.__class__.__name__

  def AddNameSpace(self, ns):
    """Add namespace declaration(s) to the current object.

    Args:
      ns: a namespace dict to add.
    """
    for k, v in ns.iteritems():
      setattr(self, k, v)

  def DelNameSpace(self, ns_name):
    """Remove namespace declaration from the current object.

    Args:
      ns_name: a string indicating the namespace.

    Returns:
      boolean: whether or not the method is done successfully.

    Raises:
      OBJsonTypeError: When the pass in param isn't string Type.
    """
    if type(ns_name) in (types.StringType, types.UnicodeType):
      if ns_name.lower() in self.GetAttrs and type(getattr(self, ns_name) \
          in (types.StringType, types.UnicodeType)):
        del self.__dict__[ns_name]
        return True
      else:
        return False
    else:
      raise OBJsonTypeError, 'pass in value %s is not namespace string' % \
            ns_name

  def UpdateNameSpace(self, ns):
    """Replace namespace declaration with the passi-in value.

    Args:
      ns: a namespace dict used to update the existing one.
    
    Returns:
      boolean: True when successfully updated, otherwise, return False.
    """
    for k, v in ns:
      if self.DelNameSpace(k):
        self.AddNameSpace(ns)
      else:
        return False
    return True

  def HasChild(self, child):
    """Function to return whether or not the current object
       node has child as a child node.

    Args:
      child: Either a child node name string or a OBJson object.

    Returns:
      boolean

    Raises:
      OBJsonNoKeyFoundError
    """
    if isinstance(child, OBJson):
      name = child.GetName
    elif type(child) is types.StringType:
      name = str(child)
    elif type(child) is types.ListType:
      for i in child:
        name = i.GetName
        # elements in list are all the same type of child, so
        # get the first one and exit the loop
        break
    else:
      raise OBJsonNoKeyFoundError, 'No such node name %s found' % child

    if name in self.GetAttrs:
      return True
    else:
      return False
  
  def GetAttrib(self):
    """Return a tuple of attributes of the current OBJson object.
    
    This returns the attributes whose values are just plain text not
    sub OBJson objects or list. It is simply an opposite way of GetChildren
    method. 
    """
    alist = []
    
    for i in self.GetAttrs:
      if i.find('__parent') == -1:
        if not isinstance(getattr(self, i), OBJson) \
           and type(getattr(self, i)) is not types.ListType:
          alist.append((i, getattr(self, i)))
    
    return alist
  
  GetNoChildren = GetAttrib

  def GetChildren(self):
    """Function to return a list of child objects.

    Returns:
      A tuple of child objects. Otherwise, an empty tuple.
    """
    alist = []

    for i in self.GetAttrs:
      if i.find('__parent') == -1:
        if isinstance(getattr(self, i), OBJson):
          alist.append(getattr(self, i))
        elif type(getattr(self, i)) is types.ListType:
          for j in getattr(self, i):
            alist.append(j)
    if alist:
      return tuple(alist)
    else:
      return None

  def _HandleChild(self, action, name, obj=None, robj=None):
    """The actual OBJson child node handler.

    Take child manipulation actions including, append, insert,
    create, update, delete

    Args:
      action: one of the above actions to take
      name: the child node attribute name
      obj: (optional)the child node object.
      robj: (optional)a reference object as of where the current child
            node should be placed when taking action 'insert'

    Raises:
      OBJsonValueError, ValueError: when the refobj object isn't in the
      list.
    """
    if obj:
      if isinstance(obj, OBJson):
        oj = obj
      else:
        oj = genpyobj(name, obj, self)
      oj.ParentObj = self

      if action == 'append':
        if type(getattr(self, name)) is types.ListType:
          getattr(self, name).append(oj)
        else:
          tmp = [getattr(self, name)]
          tmp.append(oj)
          setattr(self, name, tmp)
      elif action == 'create' or action == 'update':
        setattr(self, name, oj)
      elif action == 'insert':
        if type(getattr(self, name)) is types.ListType:
          alist = getattr(self, name)
          try:
            ind = alist.index(robj)
          except (OBJsonValueError, ValueError), e:
            print '%s does not have %s in the list. Error: %s' % \
              (name, str(robj), e)
          else:
            alist.insert(ind, oj)
        else:
          alist = [oj, robj]
        setattr(self, name, alist)
    else:
      if action == 'delete':
        del self.__dict__[name]

  def AppendChild(self, name, src):
    """Add a child element to this OBJson object.

    The source must be either a dict or a converted OBJson object.
    current object MUST have an existing child already.

    1. When src is a dict. It will be sent to the factory
       to create the OBJson object using the name provided.
    2. When the src is an OBJson object, it will be linked directly.

    In both these cases:
    1. If this child does not exist yet (meaning does
       not have that object class name as the attribute), the
       object will be linked directly.
    2. If there is a child, then it will create
       a list and assign the existing child(ren) and the newly
       created child to this list and link it.
    3. If there has a list of child, then it will
       simply just append the new OBJson object to that list.

    Args:
      name: the attribute name of the child node.
      src: either a dict or an OBJson object.
           if not provide, default creates an empty dict object.
    
    Raises:
      OBJsonAttributeError, when there is no an existing child.
      OBJsonTypeError, the pass-in param isn't something parseable.

    Example:
      self.AppendChild('author',{'name':'foo','email':'foo@gmail.com'})
      self.AppendChild('author', au) # au is an author object.
    """
    # If src is None, then an empty dict will be assigned.
    # bug/fixme: need to find the correct parent. (done)
    # with a correct parent, things can be forced to be assigned
    # correctly. (at least at some level)
    # TODO: add support to add objson objects directly. (done)
    if name not in self.GetAttrs:
      raise OBJsonAttributeError, \
            'No such child node %s in the current node. If you insist to add' \
            ' this child node, please use CreateChild() method' % name
    else:
      if not isinstance(src, OBJson) and type(src) is not types.DictType:
        raise OBJsonTypeError, 'A child node has to be either an OBJson object' \
          ' or a dict'
      else:
        self._HandleChild('append', name, obj=src)

  def CreateChild(self, name, src):
    """Create a new child under the current object.

    If child name exists, then pass it to AppendChild method.

    Args:
      name: the new child name
      src: source data to attach to this child node.
             data type can be either a dict or OBJson object.
    """
    if name in self.GetAttrs:
      warnings.warn('child already existing, use AppendChild method instead')
      self.AppendChild(name, src)
    else:
      if not isinstance(src, OBJson) and type(src) is not types.DictType:
        raise OBJsonTypeError, 'A child node has to be either an OBJson object' \
          ' or a dict'
      else:
        self._HandleChild('create', name, obj=src)

  def InsertChild(self, name, src, refobj):
    """Function to insert a child node to an existing child node list.

    If not, create the list first. refobj is the child node where
    the new child node will insert before.

    Args:
      name: the child name.
      refobj: The reference object where this new child node will be 
              inserted before.
      src: the data of the child. Can be either a dict or an OBJson object.

    Raises:
      OBJsonTypeError: when passed in refobj is not an OBJson object.
    """
    if name not in self.GetAttrs:
      raise OBJsonAttributeError, \
            'No such child node %s in the current node. If you insist to add' \
            ' this child node, please use CreateChild() method' % name
    else:
      # The reference object is not a objson object, raise error.
      if not isinstance(refobj, OBJson):
        raise OBJsonTypeError, '%s is not an OBJson object' % refobj
      # If the child isn't a list and the refobj isn't the
      # child node, raise error. This is for security reason
      # where a client wouldn't accidentially be referenced.
      elif type(getattr(self, name)) is not types.ListType and \
          getattr(self, name) is not refobj:
        raise OBJsonTypeError, '%s is not the child of %s' % \
             (str(refobj), name)
      else:
        self._HandleChild('insert', name, obj=src, robj=refobj)

  def UpdateChild(self, name, src):
    """Function to update a child object.

    Similar function of CreateChild. Instead of adding, it replace the existing
    subobject(child) to the new src. Implying that child MUST be pre-existing
    already.

    Args:
      name: the child name

      src:
        The new src. The format can be either
        a dict object or an OBJson object.
        If src is None, the child value will be
        vanished.
    """
    self._HandleChild('update', name, obj=src)

  def DeleteChild(self, child):
    """Function to remove a child object from the OBJson structure.

    This will remove the object itself and also the key pointing to the object.

    Args:
      name: the child need to remove.

    Raises:
      OBJsonNoKeyFound: if there is no such child attribute found.
    """
    child_name = child.GetName
    if child.ParentObj:
      the_self = getattr(child.ParentObj, child_name)
    else:
      the_self = None
    if type(the_self) is types.ListType:
      ind = the_self.index(child)
      the_self.pop(ind)
    else:
      try:
        self._HandleChild('delete', child_name)
      except:
        raise OBJsonNoKeyFoundError, 'No child name %s found in %s' % \
          (child_name, self)
      else:
        child.ParentObj = None

  def MakeClone(self):
    """Function to return a copy of self object.

    A deepcopy is necessary here since we want to create
    a clone that is totally unrelated to the original object.

    Returns:
      A deep copy of self. meaning every objects underneath
      and itself are copied to another memroy address.
      changing one object will not affact another one at all.
    """
    tmp = self.ParentObj
    self.ParentObj = None
    newobj = copy.deepcopy(self)
    self.ParentObj = tmp
    return newobj


def genpyobj(name, src, parent):
  """Function to generate OBJson objects dynamically.

  The factory function.

  Args:
    name: class name used to create the subclass
    src: cjson converted JSON feed or part of it.
    parent: The parent object who will be assigned an attribute
            using this 'name' and linked to the result object.

  Returns:
    An OBJson object.
  """
  if name.find('$') != -1:
    klass = name.replace('$','___')
  else:
    klass = name

  try:
    return globals()[klass](src, parent)
  except:
    exec 'class %s(OBJson): pass' % klass in globals()
    return globals()[klass](src, parent)

# vim :set ts=2 sw=2 expandtab
# The End
